
使用ORC的分层方法，很容易构建针对需求定制的JIT编译器。没有放之四海而皆准的JIT编译器，本章的第一节给出了一些示例。让我们看一下如何从头开始设置JIT编译器。

ORC API使用堆叠在一起的层。最低层是对象链接层，由llvm::orc::RTDyldObjectLinkingLayer类表示，负责链接内存中的对象并将其转换为可执行代码。此任务所需的内存由MemoryManager接口的实例管理，有一个默认的实现，也可以使用自定义版本。

对象链接层之上是编译层，负责创建内存中的对象文件。llvm::orc::IRCompileLayer类接受IR模块作为输入，并将其编译为对象文件。IRCompileLayer类是IRLayer类的子类，IRLayer类是接受LLVM IR的层实现的通用类。

这两层已经构成了JIT编译器的核心:添加一个LLVM IR模块作为输入，该模块在内存中进行编译和链接。为了增加功能，可以在两层之上合并更多的层。

例如，CompileOnDemandLayer类拆分一个模块，以便只编译请求的函数，这可以用来实现延迟编译，CompileOnDemandLayer类也是IRLayer类的一个子类。IRTransformLayer类，也是IRLayer类的一个子类，以一种非常通用的方式，允许对模块应用转换。

另一个重要的类是ExecutionSession类，这个类表示一个正在运行的JIT程序。这个类管理JITDylib符号表，提供符号查找功能，并跟踪使用的资源管理器。

JIT编译器的通用方式:

\begin{enumerate}
\item
初始化ExecutionSession类的实例。

\item
初始化层，至少由RTDyldObjectLinkingLayer类和IRCompileLayer类组成。

\item
创建第一个JITDylib符号表，通常使用main或类似的名称。
\end{enumerate}

JIT编译器的一般用法也非常简单:

\begin{enumerate}
\item
在符号表中添加一个IR模块。

\item
查找一个符号，触发相关函数的编译，也可能触发整个模块的编译。

\item
执行函数。
\end{enumerate}

下一小节中，我们将按照通用方式实现JIT编译器类。

\mySubsubsection{9.5.1.}{创建JIT编译器类}

为了保持JIT编译器类的实现简单，所有内容都放在JIT.h中，可以放在名为JIT的目录中。与使用LLJIT相比，类的初始化要复杂一些。由于要处理可能的错误，调用构造函数之前，需要一个工厂方法来预先创建一些对象。创建类的步骤如下:

\begin{enumerate}
\item
首先使用JIT\_H预处理器定义来保护头文件不可多次包含:

\begin{cpp}
#ifndef JIT_H
#define JIT_H
\end{cpp}

\item
首先，需要一些包含文件，其中的大多数都提供了一个与头文件同名的类。Core.h头文件提供了几个基本类，包括ExecutionSession类。ExecutionUtils.h头文件提供了DynamicLibrarySearchGenerator类来搜索库中的符号，CompileUtils.h头文件提供了ConcurrentIRCompiler类:

\begin{cpp}
#include "llvm/Analysis/AliasAnalysis.h"
#include "llvm/ExecutionEngine/JITSymbol.h"
#include "llvm/ExecutionEngine/Orc/CompileUtils.h"
#include "llvm/ExecutionEngine/Orc/Core.h"
#include "llvm/ExecutionEngine/Orc/ExecutionUtils.h"
#include "llvm/ExecutionEngine/Orc/IRCompileLayer.h"
#include "llvm/ExecutionEngine/Orc/IRTransformLayer.h"
#include "llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h"
#include "llvm/ExecutionEngine/Orc/Mangling.h"
#include "llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h"
#include "llvm/ExecutionEngine/Orc/TargetProcessControl.h"
#include "llvm/ExecutionEngine/SectionMemoryManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Support/Error.h"
\end{cpp}

\item
声明一个新类。我们的新类称为JIT:

\begin{cpp}
class JIT {
\end{cpp}

\item
私有数据成员反映了ORC层和一些辅助类。ExecutionSession、ObjectLinkingLayer、CompileLayer、OptIRLayer和MainJITDylib实例表示正在运行的JIT程序、层和符号表，TargetProcessControl实例用于与JIT目标进程进行交互。这可以是同一进程，同一机器上的另一个进程，或者不同机器上的远程进程，可能具有不同的架构。DataLayout和MangleAndInterner类需要以正确的方式修改符号的名称。符号名称可以内化，所以所有相等的名称具有相同的地址。所以检查两个符号名是否相等，比较地址就足够了，这个操作非常快:

\begin{cpp}
    std::unique_ptr<llvm::orc::TargetProcessControl> TPC;
    std::unique_ptr<llvm::orc::ExecutionSession> ES;
    llvm::DataLayout DL;
    llvm::orc::MangleAndInterner Mangle;
    std::unique_ptr<llvm::orc::RTDyldObjectLinkingLayer>
        ObjectLinkingLayer;
    std::unique_ptr<llvm::orc::IRCompileLayer>
        CompileLayer;
    std::unique_ptr<llvm::orc::IRTransformLayer>
        OptIRLayer;
    llvm::orc::JITDylib &MainJITDylib;
\end{cpp}

\item
初始化分为三个部分。C++中，构造函数不能返回错误。简单且推荐的解决方案是创建一个静态工厂方法，可以在构造对象之前进行错误处理。层的初始化更为复杂，所以也为它们引入了工厂方法。

create()工厂方法中，首先创建一个SymbolStringPool实例，该实例用于实现字符串内部化，并由多个类共享。为了控制当前流程，创建了一个SelfTargetProcessControl实例。若想要针对不同的流程，则需要更改此实例。

接下来，构造一个JITTargetMachineBuilder实例，为此需要知道JIT进程的目标三元组，再向目标机器生成器查询数据布局。若构建器无法基于提供的三元组实例化目标机器，则此步骤可能会失败，例如：因为对此目标的支持没有编译到LLVM库中:

\begin{cpp}
public:
    static llvm::Expected<std::unique_ptr<JIT>> create() {
        auto SSP =
            std::make_shared<llvm::orc::SymbolStringPool>();
        auto TPC =
            llvm::orc::SelfTargetProcessControl::Create(SSP);
        if (!TPC)
            return TPC.takeError();
        llvm::orc::JITTargetMachineBuilder JTMB(
            (*TPC)->getTargetTriple());
        auto DL = JTMB.getDefaultDataLayoutForTarget();
        if (!DL)
            return DL.takeError();
\end{cpp}

\item
此时，已经处理了所有可能失败的调用，现在可以初始化ExecutionSession实例了。最后，使用所有实例化的对象调用JIT类的构造函数，并将结果返回给调用者:

\begin{cpp}
        auto ES =
            std::make_unique<llvm::orc::ExecutionSession>(
                std::move(SSP));

        return std::make_unique<JIT>(
            std::move(*TPC), std::move(ES), std::move(*DL),
            std::move(JTMB));
    }
\end{cpp}

\item
JIT类的构造函数将传递的参数移动到私有数据成员，层对象是通过调用带有create前缀的静态工厂名称来构造的。此外，每个层工厂方法都需要对ExecutionSession实例的引用，该实例将层连接到正在运行的JIT会话。除了位于层堆栈底部的对象链接层外，每一层都需要引用前一层，说明了堆叠顺序:

\begin{cpp}
JIT(std::unique_ptr<llvm::orc::ExecutorProcessControl>
            EPCtrl,
    std::unique_ptr<llvm::orc::ExecutionSession>
        ExeS,
    llvm::DataLayout DataL,
    llvm::orc::JITTargetMachineBuilder JTMB)
    : EPC(std::move(EPCtrl)), ES(std::move(ExeS)),
        DL(std::move(DataL)), Mangle(*ES, DL),
        ObjectLinkingLayer(std::move(
            createObjectLinkingLayer(*ES, JTMB))),
        CompileLayer(std::move(createCompileLayer(
            *ES, *ObjectLinkingLayer,
            std::move(JTMB)))),
        OptIRLayer(std::move(
            createOptIRLayer(*ES, *CompileLayer))),
        MainJITDylib(
            ES->createBareJITDylib("<main>")) {
\end{cpp}

\item
构造函数中，添加了一个生成器，用于在当前进程中搜索符号。GetForCurrentProcess()方法很特殊，返回值包装在一个Expected<>模板中，表明也可以返回一个Error对象。由于我们知道不会发生错误，所以当前进程最终将运行!使用cantFail()函数展开结果，若发生错误，该函数将终止应用程序:

\begin{cpp}
    MainJITDylib.addGenerator(llvm::cantFail(
        llvm::orc::DynamicLibrarySearchGenerator::
            GetForCurrentProcess(DL.getGlobalPrefix())));
}
\end{cpp}

\item
要创建一个对象链接层，需要提供一个内存管理器。这里，我们坚持使用默认的SectionMemoryManager类，也可以提供不同的实现:

\begin{cpp}
    static std::unique_ptr<
        llvm::orc::RTDyldObjectLinkingLayer>
    createObjectLinkingLayer(
        llvm::orc::ExecutionSession &ES,
        llvm::orc::JITTargetMachineBuilder &JTMB) {
    auto GetMemoryManager = []() {
        return std::make_unique<
            llvm::SectionMemoryManager>();
    };
    auto OLLayer = std::make_unique<
        llvm::orc::RTDyldObjectLinkingLayer>(
        ES, GetMemoryManager);
\end{cpp}

\item
Windows上使用的通用对象文件格式(COFF)对象文件格式存在轻微的复杂性，此文件格式不允许将函数标记为导出。这随后导致对象链接层内部检查失败：将符号中存储的标志与IR中的标志进行比较，由于缺少导出标记，导致不匹配，解决方案是只覆盖这种文件格式的标志。这就完成了对象层的构造，对象会返回给调用者:

\begin{cpp}
   if (JTMB.getTargetTriple().isOSBinFormatCOFF()) {
       OLLayer
            ->setOverrideObjectFlagsWithResponsibilityFlags(
                true);
       OLLayer
            ->setAutoClaimResponsibilityForObjectSymbols(
                true);
   }
   return OLLayer;
}
\end{cpp}

\item
要初始化编译器层，需要一个IRCompiler实例，IRCompiler实例负责将一个IR模块编译成一个object文件。若JIT编译器不使用线程，则可以使用SimpleCompiler类，使用给定的目标机器编译IR模块。TargetMachine类线程不安全，因此SimpleCompiler类也不是。为了支持多线程编译，使用ConcurrentIRCompiler类，为每个要编译的模块创建一个新的TargetMachine实例。这种方法解决了多线程的问题:

\begin{cpp}
    static std::unique_ptr<llvm::orc::IRCompileLayer>
    createCompileLayer(
            llvm::orc::ExecutionSession &ES,
            llvm::orc::RTDyldObjectLinkingLayer &OLLayer,
            llvm::orc::JITTargetMachineBuilder JTMB) {
        auto IRCompiler = std::make_unique<
            llvm::orc::ConcurrentIRCompiler>(
            std::move(JTMB));
        auto IRCLayer =
            std::make_unique<llvm::orc::IRCompileLayer>(
                ES, OLLayer, std::move(IRCompiler));
        return IRCLayer;
    }
\end{cpp}

\item
我们没有将IR模块直接编译为机器码，而是安装了一个层来首先优化IR。这是一个深思熟虑的设计决策:将JIT编译器转换为优化的JIT编译器，生成更快的代码，而生成代码需要更长的时间，所以用户需要等待。我们没有添加延迟编译，所以当只查找一个符号时，整个模块都会编译。用户看到代码执行之前，可能会等待很久。

\begin{myNotic}{Note}
所有情况下，引入惰性编译并不是一个合适的解决方案。惰性编译是通过将每个函数移动到它自己的模块中来实现的，该模块在查找函数名时进行编译。防止了诸如内联之类的过程间优化，因为内联传递需要访问调用函数的体才能内联。用户看到延迟编译的启动速度更快，但生成的代码并不是最优的。这些设计决策取决于预期的用途。我们决定使用快速代码，接受较慢的启动时间，所以优化层本质上是一个转换层。
\end{myNotic}

IRTransformLayer类将转换委托给函数——我们的例子中，委托给了optimizeModule函数:

\begin{cpp}
    static std::unique_ptr<llvm::orc::IRTransformLayer>
    createOptIRLayer(
            llvm::orc::ExecutionSession &ES,
            llvm::orc::IRCompileLayer &CompileLayer) {
        auto OptIRLayer =
        std::make_unique<llvm::orc::IRTransformLayer>(
            ES, CompileLayer,
            optimizeModule);
        return OptIRLayer;
    }
\end{cpp}

\item
optimizeModule()函数是IR模块转换的一个示例。该函数获取要转换的模块作为参数，并返回转换后的IR模块版本。由于JIT编译器可能运行多个线程，所以IR模块可封装在ThreadSafeModule实例中:

\begin{cpp}
    static llvm::Expected<llvm::orc::ThreadSafeModule>
    optimizeModule(
        llvm::orc::ThreadSafeModule TSM,
        const llvm::orc::MaterializationResponsibility
        &R) {
\end{cpp}

\item
为了优化IR，回顾以下第7章，需要PassBuilder实例来创建优化流水线。首先，定义两个分析管理器，然后在通道构建器中注册，再用O2级别的默认优化流水线填充ModulePassManager实例。这又是一个设计决策:O2级别生成的机器码已经很快了，但O3级别生成的代码甚至更快。接下来，我们在模块上运行流水线。最后，优化后的模块返回给调用者:

\begin{cpp}
    TSM.withModuleDo([](llvm::Module &M) {
        bool DebugPM = false;
        llvm::PassBuilder PB(DebugPM);
        llvm::LoopAnalysisManager LAM(DebugPM);
        llvm::FunctionAnalysisManager FAM(DebugPM);
        llvm::CGSCCAnalysisManager CGAM(DebugPM);
        llvm::ModuleAnalysisManager MAM(DebugPM);
        FAM.registerPass(
            [&] { return PB.buildDefaultAAPipeline(); });
        PB.registerModuleAnalyses(MAM);
        PB.registerCGSCCAnalyses(CGAM);
        PB.registerFunctionAnalyses(FAM);
        PB.registerLoopAnalyses(LAM);
        PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);
        llvm::ModulePassManager MPM =
            PB.buildPerModuleDefaultPipeline(
                llvm::PassBuilder::OptimizationLevel::O2,
                DebugPM);
        MPM.run(M, MAM);
    });

    return TSM;
}
\end{cpp}

\item
JIT类的客户机需要一种添加IR模块的方法，通过addIRModule()函数提供了这种方法。回想一下我们创建的层堆栈:必须将IR模块添加到顶层;否则，会不绕过一些层。这是一个不容易发现的编程错误:若OptIRLayer成员用CompileLayer成员取代，JIT类仍然工作，但不是作为优化JIT，因为已经绕过了这一层。对于这个小实现来说无关紧要，但是在一个大JIT优化中，我们会引入一个函数来返回顶层:

\begin{cpp}
    llvm::Error addIRModule(
            llvm::orc::ThreadSafeModule TSM,
            llvm::orc::ResourceTrackerSP RT = nullptr) {
        if (!RT)
            RT = MainJITDylib.getDefaultResourceTracker();
        return OptIRLayer->add(RT, std::move(TSM));
    }
\end{cpp}

\item
同样，JIT类的客户机需要一种查找符号的方法。我们把它委托给ExecutionSession实例，传递一个对主符号表的引用和请求符号的内部名称:

\begin{cpp}
    llvm::Expected<llvm::orc::ExecutorSymbolDef>
    lookup(llvm::StringRef Name) {
        return ES->lookup({&MainJITDylib},
                            Mangle(Name.str()));
    }
\end{cpp}
\end{enumerate}

这个JIT类的初始化可能很棘手，它涉及到JIT类的工厂方法和构造函数调用，以及每个层的工厂方法。尽管这个发行版是由C++的限制造成的，但代码本身很简单。

接下来，将使用新的JIT编译器类来实现一个简单的命令行实用程序，该实用程序将LLVM IR文件作为输入。

\mySubsubsection{9.5.2.}{使用新的JIT编译器类}

首先在与JIT.h文件相同的目录下创建一个名为JIT.cpp的文件，并在此源文件中添加以下内容:

\begin{enumerate}
\item
首先，包含几个头文件。必须包含JIT.h来使用我们的新类，还必须包含IRReader.h头文件，定义了一个读取LLVM IR文件的函数，CommandLine.h头文件允许我们以LLVM风格解析命令行选项，还需要InitLLVM.h进行工具的基本初始化。最后，TargetSelect.h需要用于初始化本机目标:

\begin{cpp}
#include "JIT.h"
#include "llvm/IRReader/IRReader.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/TargetSelect.h"
\end{cpp}

\item
接下来，我们将llvm命名空间添加到当前作用域:

\begin{cpp}
using namespace llvm;
\end{cpp}

\item
JIT工具在命令行上只需要一个输入文件，用cl::opt<>类声明它:

\begin{cpp}
static cl::opt<std::string>
    InputFile(cl::Positional, cl::Required,
        cl::desc("<input-file>"));
\end{cpp}

\item
为了读取IR文件，调用parseIRFile()函数，该文件可以是文本IR表示或位码文件。该函数返回一个指向所创建模块的指针。因为可以解析文本IR文件，错误处理有点不同，这在语法上不一定正确。最后，SMDiagnostic实例保存语法错误时的错误信息。若发生错误，则输出错误消息，并退出应用程序:

\begin{cpp}
std::unique_ptr<Module>
loadModule(StringRef Filename, LLVMContext &Ctx,
            const char *ProgName) {
    SMDiagnostic Err;
    std::unique_ptr<Module> Mod =
        parseIRFile(Filename, Err, Ctx);
    if (!Mod.get()) {
        Err.print(ProgName, errs());
        exit(-1);
    }
    return Mod;
}
\end{cpp}

\item
jitmain()函数放在loadModule()方法之后，函数设置我们的JIT引擎并编译一个LLVM IR模块。该函数需要带有IR的LLVM模块来执行。这个模块还需要LLVM上下文类，上下文类包含重要的类型信息。因为我们的目标是调用main()函数，所以也传递了常用的argc和argv参数:

\begin{cpp}
Error jitmain(std::unique_ptr<Module> M,
    std::unique_ptr<LLVMContext> Ctx,
    int argc, char *argv[]) {
\end{cpp}

\item
接下来，创建前面构造的JIT类的一个实例。若发生错误，则返回相应的错误消息:

\begin{cpp}
auto JIT = JIT::create();
if (!JIT)
    return JIT.takeError();
\end{cpp}

\item
将模块添加到主JITDylib实例中，再次将模块和上下文包装在ThreadSafeModule实例中。若发生错误，则返回一条错误消息:

\begin{cpp}
if (auto Err = (*JIT)->addIRModule(
        orc::ThreadSafeModule(std::move(M),
                              std::move(Ctx))))
    return Err;
\end{cpp}

\item
查找主符号，此符号必须在命令行上给出的IR模块中。查找触发该IR模块的编译。若在IR模块中引用了其他符号，则使用上一步中添加的生成器进行解析。结果是ExecutorAddr类，表示执行进程的地址:

\begin{cpp}
    llvm::orc::ExecutorAddr MainExecutorAddr = MainSym->getAddress();
    auto *Main = MainExecutorAddr.toPtr<int(int, char**)>();
\end{cpp}

\item
可以调用IR模块中的main()函数，并传递函数期望的argc和argv参数:

\begin{cpp}
    (void)Main(argc, argv);
\end{cpp}

\item
函数执行成功后进行报告:

\begin{cpp}
   return Error::success();
}
\end{cpp}

\item
实现了jitmain()函数之后，添加了main()函数，其初始化工具和本机目标并解析命令行:

\begin{cpp}
int main(int argc, char *argv[]) {
    InitLLVM X(argc, argv);
    InitializeNativeTarget();
    InitializeNativeTargetAsmPrinter();
    InitializeNativeTargetAsmParser();

    cl::ParseCommandLineOptions(argc, argv, "JIT\n");
\end{cpp}

\item
初始化LLVM上下文类，并加载命令行中指定的IR模块:

\begin{cpp}
    auto Ctx = std::make_unique<LLVMContext>();
    std::unique_ptr<Module> M =
        loadModule(InputFile, *Ctx, argv[0]);
\end{cpp}

\item
加载IR模块后，可以调用jitmain()函数。为了处理错误，使用ExitOnError实用程序类来打印错误消息，并在遇到错误时退出应用程序。我们还设置了一个带有应用程序名称，其输出在错误消息之前:

\begin{cpp}
    ExitOnError ExitOnErr(std::string(argv[0]) + ": ");
    ExitOnErr(jitmain(std::move(M), std::move(Ctx),
                      argc, argv));
\end{cpp}

\item
若控制流达到这一点，则IR已成功执行。返回0表示成功:

\begin{cpp}
    return 0;
}
\end{cpp}
\end{enumerate}

现在，可以通过编译一个简单的示例来测试新实现的JIT编译器，该示例输出Hello World!到控制台。底层新类使用一个固定的优化级别，因此对于足够大的模块，可以注意到启动和运行时的差异。

要构建JIT编译器，可以按照在用LLJIT实现自己的JIT编译器一节末尾所做的相同的CMake步骤，只需要确保JIT.cpp源文件正在用正确的库进行编译。

\begin{cmake}
add_executable(JIT JIT.cpp)
include_directories(${CMAKE_SOURCE_DIR})
target_link_libraries(JIT ${llvm_libs})
\end{cmake}

再切换到build目录并编译应用程序:

\begin{shell}
$ cmake –G Ninja <path to jit source directory>
$ ninja
\end{shell}

我们的JIT工具现在可以使用了。一个简单的Hello World!程序可以用C编写，如下所示:

\begin{shell}
$ cat main.c
#include <stdio.h>

int main(int argc, char** argv) {
    printf("Hello world!\n");
    return 0;
}
\end{shell}

可以用下面的命令将Hello World C源代码编译成LLVM IR:

\begin{shell}
$ clang -S -emit-llvm main.c
\end{shell}

记住——我们将C源代码编译成LLVM IR，因为JIT编译器接受IR文件作为输入。最后，可以用IR示例调用JIT编译器:

\begin{shell}
$ JIT main.ll
Hello world!
\end{shell}









